﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Player
{
    public class CachedBufferEntry
    {
        private byte[] m_buffer = null;
        public byte[] Buffer { get { return m_buffer; } }

        private bool m_inUse = false;
        public bool InUse
        {
            get { lock (this) { return m_inUse; } }
            set { lock (this) { m_inUse = value; } }
        }

        public bool Capture()
        {
            lock (this)
            {
                if (!m_inUse)
                {
                    m_inUse = true;
                    return true;
                }

                return false;
            }
        }

        public CachedBufferEntry(byte[] buffer)
        {
            m_buffer = buffer;
        }

        public CachedBufferEntry(int size)
        {
            m_buffer = new byte[size];
        }

        public static explicit operator byte[](CachedBufferEntry buffer)
        {
            return buffer.m_buffer;
        }
    }

    public class CachedBuffer : IDisposable
    {
        private CachedBufferEntry m_entry = null;

        public CachedBuffer(CachedBufferEntry entry)
        {
            m_entry = entry;
        }

        public byte[] Buffer { get { return m_entry.Buffer; } }
        public static explicit operator byte[](CachedBuffer buffer)
        {
            return (byte[])buffer.m_entry;
        }

        private bool m_disposed = false;
        protected virtual void Dispose(bool disposing)
        {
            if (!m_disposed)
            {
                if (disposing)
                {
                    m_entry.InUse = false;
                    m_entry = null;
                }

                m_disposed = true;
            }
        }

        ~CachedBuffer()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }

    // This is the actual cache and allocator, should not be used directly,
    //  instead use one of the collection or cache buffer classes above.
    public static class CachedBufferAllocator
    {
        private class Bucket
        {
            private List<CachedBufferEntry> m_bits = new List<CachedBufferEntry>();
            public int Size { get; private set; }

            public Bucket(int bucketSize)
            {
                Size = bucketSize;
            }

            /// <summary>
            /// Search a Cached Buffer Entry which is not in use or create a new Cached Buffer Entry and set it in use.
            /// </summary>
            /// <returns></returns>
            public CachedBufferEntry Get()
            {
                for (int x = 0; x < m_bits.Count; x++)
                {
                    if (m_bits[x].Capture())
                    {
                        return m_bits[x];
                    }
                }
          
                CachedBufferEntry entry = new CachedBufferEntry(Size);
                entry.InUse = true;
                m_bits.Add(entry);
                return entry;
            }

            /// <summary>
            /// the count in used Cache Buffer Entry * Size
            /// </summary>
            /// <returns></returns>
            public int CalculateAllocatedBytes()
            {
                int inUse = 0;
                for (int x = 0; x < m_bits.Count; x++)
                {
                    if (m_bits[x].InUse)
                    {
                        inUse++;
                    }
                }

                return (Size * inUse);
            }
        }

        private static object m_mutex = new object();
        private static List<Bucket> m_buckets = new List<Bucket>();

        private const int BUCKET_WIDTH = 128 * 1024;
        private const int INITIAL_BUCKET_COUNT = 10;
        private const int MAX_USED_BEFORE_COLLECTION = 512 * 1024 * 1024;

        static CachedBufferAllocator()
        {
            int curBucketSize = BUCKET_WIDTH;
            for (int x = 0; x < INITIAL_BUCKET_COUNT; x++)
            {
                m_buckets.Add(new Bucket(curBucketSize));
                curBucketSize += BUCKET_WIDTH;
            }
        }

        // Get a buffer, no wrapping for disposal etc.  User
        //  must set CachedBufferEntry.InUse = false when done
        //  or this object will leak!
        public static CachedBufferEntry GetUnscoped(int size)
        {
            lock (m_mutex)
            {
                return GetEntry(size);
            }
        }

        // Get a buffer which will be used within a Using() scope
        //  or released with an explicit .Dispose() call when no
        //  longer needed.
        public static CachedBuffer GetCachedBuffer(int size)
        {
            lock (m_mutex)
            {
                return new CachedBuffer(GetEntry(size));
            }
        }

        // NOTE THIS IS SLOW! Use for testing only!
        public static int CalculateInUseBytes()
        {
            lock (m_mutex)
            {
                int allocated = 0;
                for (int x = 0; x < m_buckets.Count; x++)
                {
                    allocated += m_buckets[x].CalculateAllocatedBytes();
                }

                return allocated;
            }
        }

        private static CachedBufferEntry GetEntry(int size)
        {
            int lastBucketSize = 0;
            for (int x = 0; x < m_buckets.Count; x++)
            {
                lastBucketSize = m_buckets[x].Size;
                if (size < lastBucketSize)
                {
                    return m_buckets[x].Get();
                }
            }

            // Need to tack on more buckets
            while (size > lastBucketSize)
            {
                lastBucketSize += BUCKET_WIDTH;
                m_buckets.Add(new Bucket(lastBucketSize));
            }

            return m_buckets[m_buckets.Count - 1].Get();
        }
    }

    public class CachedBufferCollection : IDisposable
    {
        private List<CachedBufferEntry> m_buffers = new List<CachedBufferEntry>();

        public byte[] Get(int size)
        {
            lock (this)
            {
                CachedBufferEntry entry = CachedBufferAllocator.GetUnscoped(size);
                m_buffers.Add(entry);
                return entry.Buffer;
            }
        }

        private bool m_disposed = false;
        protected virtual void Dispose(bool disposing)
        {
            if (!m_disposed)
            {
                if (disposing)
                {
                    foreach (CachedBufferEntry entry in m_buffers)
                    {
                        entry.InUse = false;
                    }
                    m_buffers.Clear();
                }

                m_disposed = true;
            }
        }

        ~CachedBufferCollection()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }

    // Per-thread collection of in-use cached buffers, the
    //  thread must explicitly call CleanupCurrentThread() before terminating
    //  (or on occassion) lest all buffers in use by the thread be 
    //  leaked.  When CleanupCurrentThread() is called is left to the thread in
    //  question, but should be avoided so long as any retrieved
    //  buffer is in use.  A thread segment collections on the stack using
    //  Push() and Pop() to mark start/end points of a given collection.  All
    //  calls to Get() go on the collection at the top of the stack.
    public static class CachedBufferThreadCollection
    {
        private static Dictionary<int, Stack<List<CachedBufferEntry>>> m_threadCollections = new Dictionary<int, Stack<List<CachedBufferEntry>>>();

        public static byte[] Get(int size)
        {
            lock (m_threadCollections)
            {
                int currentThread = System.Threading.Thread.CurrentThread.ManagedThreadId;
                CachedBufferEntry entry = CachedBufferAllocator.GetUnscoped(size);

                Stack<List<CachedBufferEntry>> stack = null;
                if (m_threadCollections.TryGetValue(currentThread, out stack))
                {
                    // Stack exists, must have at least 1 element, peek at the top and add to that list
                    List<CachedBufferEntry> top = stack.Peek();
                    top.Add(entry);
                }
                else
                {
                    stack = new Stack<List<CachedBufferEntry>>();
                    List<CachedBufferEntry> list = new List<CachedBufferEntry>();
                    list.Add(entry);
                    stack.Push(list);   // Must always have at least one item on stack                    
                    m_threadCollections.Add(currentThread, stack);
                }

                return (byte[])entry;
            }
        }

        public static void Push()
        {
            lock (m_threadCollections)
            {
                int currentThread = System.Threading.Thread.CurrentThread.ManagedThreadId;

                Stack<List<CachedBufferEntry>> stack = null;
                if (m_threadCollections.TryGetValue(currentThread, out stack))
                {
                    stack.Push(new List<CachedBufferEntry>());
                }
                else
                {
                    stack = new Stack<List<CachedBufferEntry>>();
                    stack.Push(new List<CachedBufferEntry>());
                    m_threadCollections.Add(currentThread, stack);
                }
            }
        }

        public static void Pop()
        {
            lock (m_threadCollections)
            {
                int currentThread = System.Threading.Thread.CurrentThread.ManagedThreadId;
                Stack<List<CachedBufferEntry>> stack = null;
                if (m_threadCollections.TryGetValue(currentThread, out stack))
                {
                    List<CachedBufferEntry> list = stack.Pop();
                    foreach (CachedBufferEntry entry in list)
                    {
                        entry.InUse = false;
                    }
                    list.Clear();

                    if (stack.Count == 0)
                    {
                        // No more stacks, pull us out of dict then                        
                        stack.Clear();
                        m_threadCollections.Remove(currentThread);
                    }
                }
            }
        }

        // Cleans up *all* stacks of allocs dedicated to a thread!
        public static void CleanupCurrentThread()
        {
            lock (m_threadCollections)
            {
                int currentThread = System.Threading.Thread.CurrentThread.ManagedThreadId;
                Stack<List<CachedBufferEntry>> stack = null;
                if (m_threadCollections.TryGetValue(currentThread, out stack))
                {
                    List<CachedBufferEntry> list = null;
                    while (stack.Count > 0)
                    {
                        list = stack.Pop();
                        foreach (CachedBufferEntry entry in list)
                        {
                            entry.InUse = false;
                        }
                        list.Clear();
                    }
                    stack.Clear();
                    m_threadCollections.Remove(currentThread);
                }
            }
        }
    }
}
